﻿using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;

namespace MathService
{
    public class PrecisionTimer : Component
    {
        private static readonly PrecisionTimerCaps sf_Caps;
        private IContainer components;
        private volatile bool f_Disposed;
        private volatile PrecisionTimerMode f_Mode;
        private volatile int f_Period;
        private volatile int f_Resolution;
        private ISynchronizeInvoke f_SynchronizingObject;
        private EventRaiser f_TickRaiser;
        private TimeProc f_TimeProcOneShot;
        private TimeProc f_TimeProcPeriodic;
        private int f_TimerId;

        // Methods
        static PrecisionTimer()
        {
            TimeGetDevCaps(ref sf_Caps, Marshal.SizeOf(sf_Caps));
        }

        public PrecisionTimer()
        {
            Initialize();
        }

        public PrecisionTimer(IContainer container)
        {
            container.Add(this);
            Initialize();
            InitializeComponent();
        }

        public static PrecisionTimerCaps Capabilities
        {
            get { return sf_Caps; }
        }

        [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Bindable(false)]
        public bool IsRunning { get; private set; }

        [DefaultValue(typeof(PrecisionTimerMode), "Periodic")]
        public PrecisionTimerMode Mode
        {
            get
            {
                DisposeCheck();
                return f_Mode;
            }
            set
            {
                DisposeCheck();
                f_Mode = value;
                if(!IsRunning) return;
                Stop();
                Start();
            }
        }

        public int Period
        {
            get
            {
                DisposeCheck();
                return f_Period;
            }
            set
            {
                DisposeCheck();
                if((value < Capabilities.PeriodMin) || (value > Capabilities.PeriodMax))
                    throw new ArgumentOutOfRangeException("value", value, "Multimedia Timer period out of range.");
                f_Period = value;
                if(!IsRunning) return;
                Stop();
                Start();
            }
        }

        public int Resolution
        {
            get
            {
                DisposeCheck();
                return f_Resolution;
            }
            set
            {
                DisposeCheck();
                if(value < 0)
                    throw new ArgumentOutOfRangeException("value", value, "timer resolution out of range.");
                f_Resolution = value;
                if(!IsRunning) return;
                Stop();
                Start();
            }
        }

        public ISynchronizeInvoke SynchronizingObject
        {
            get
            {
                DisposeCheck();
                return f_SynchronizingObject;
            }
            set
            {
                DisposeCheck();
                f_SynchronizingObject = value;
            }
        }

        public event EventHandler Started;

        public event EventHandler Stopped;

        public event EventHandler Tick;

        protected override void Dispose(bool disposing)
        {
            if(f_Disposed) return;
            if(IsRunning)
                Stop();
            f_Disposed = true;
            if(disposing && (components != null))
                components.Dispose();
            base.Dispose(disposing);
        }

        private void DisposeCheck() { if(f_Disposed) throw new ObjectDisposedException("Timer"); }

        ~PrecisionTimer() { if(IsRunning) TimeKillEvent(f_TimerId); }

        private void Initialize()
        {
            f_Mode = PrecisionTimerMode.Periodic;
            f_Period = Capabilities.PeriodMin;
            f_Resolution = 1;
            IsRunning = false;
            f_TimeProcPeriodic = new TimeProc(TimerPeriodicEventCallback);
            f_TimeProcOneShot = new TimeProc(TimerOneShotEventCallback);
            f_TickRaiser = new EventRaiser(OnTick);
        }

        private void InitializeComponent()
        {
            components = new Container();
        }

        private void OnStarted(EventArgs e)
        {
            var started = Started;
            if(started != null)
                started(this, e);
        }

        private void OnStopped(EventArgs e)
        {
            var stopped = Stopped;
            if(stopped != null)
                stopped(this, e);
        }

        private void OnTick(EventArgs e)
        {
            var tick = Tick;
            if(tick != null)
                tick(this, e);
        }

        public void Start()
        {
            DisposeCheck();
            if(IsRunning) return;
            f_TimerId = TimeSetEvent(Period, Resolution,
                                     (Mode == PrecisionTimerMode.Periodic) ? f_TimeProcPeriodic : f_TimeProcOneShot,
                                     0, Mode);
            if(f_TimerId == 0)
                throw new TimerException("Unable to start timer.");
            IsRunning = true;
            if((SynchronizingObject != null) && SynchronizingObject.InvokeRequired)
                SynchronizingObject.BeginInvoke(new EventRaiser(OnStarted), new object[] { EventArgs.Empty });
            else
                OnStarted(EventArgs.Empty);
        }

        public void Stop()
        {
            DisposeCheck();
            if(!IsRunning) return;
            TimeKillEvent(f_TimerId);
            IsRunning = false;
            if((SynchronizingObject != null) && SynchronizingObject.InvokeRequired)
                SynchronizingObject.BeginInvoke(new EventRaiser(OnStopped), new object[] { EventArgs.Empty });
            else
                OnStopped(EventArgs.Empty);
        }

        [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
        private static extern int TimeGetDevCaps(ref PrecisionTimerCaps Caps, int SizeOfTimerCaps);

        [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
        private static extern int TimeKillEvent(int ID);

        private void TimerOneShotEventCallback(int id, int msg, int user, int param1, int param2)
        {
            if(f_SynchronizingObject != null)
            {
                f_SynchronizingObject.BeginInvoke(f_TickRaiser, new object[] { EventArgs.Empty });
                Stop();
            }
            else
            {
                OnTick(EventArgs.Empty);
                Stop();
            }
        }

        private void TimerPeriodicEventCallback(int id, int msg, int user, int param1, int param2)
        {
            if(f_SynchronizingObject != null)
            {
                f_SynchronizingObject.BeginInvoke(f_TickRaiser, new object[] { EventArgs.Empty });
            }
            else
            {
                OnTick(EventArgs.Empty);
            }
        }

        [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
        private static extern int TimeSetEvent(int Delay, int Resolution, TimeProc Proc, int User,
                                               PrecisionTimerMode Mode);

        // Properties

        // Nested Types

        #region Nested type: EventRaiser

        private delegate void EventRaiser(EventArgs e);

        #endregion

        #region Nested type: TimeProc

        private delegate void TimeProc(int ID, int Msg, int User, int Param1, int Param2);

        #endregion
    }

    [Serializable, StructLayout(LayoutKind.Sequential)]
    public struct PrecisionTimerCaps
    {
        public int PeriodMin;
        public int PeriodMax;
    }

    [Serializable]
    public enum PrecisionTimerMode : byte
    {
        OneShot = 0,
        Periodic = 1
    }

    [Serializable]
    public class TimerException : ApplicationException
    {
        // Methods
        public TimerException()
        {
        }

        public TimerException(string message)
            : base(message)
        {
        }

        protected TimerException(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        public TimerException(string message, Exception inner)
            : base(message, inner)
        {
        }
    }
}